package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.constant.MessageConstant;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClientUtil;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private CacheClientUtil cacheClientUtil;

    /**
     * 根据id查询商铺信息
     * @param id
     * @return
     */
    @Override
    public Result findShopById(Long id) {
        //缓存穿透问题
        Shop shop = cacheClientUtil.findByIdWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id,
                Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        //缓存击穿问题
        //1.互斥锁解决缓存击穿问题
        //Shop shop = findShopByIdWithMutex(id);

        //2.逻辑过期解决缓存击穿问题
        //Shop shop = findShopByIdWithLogicExpiration(id);
        //Shop shop = cacheClientUtil.findByIdWithLogicalExpiration(RedisConstants.CACHE_SHOP_KEY, id,
//                Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        if (shop == null) {
            //返回错误信息
            return Result.error(MessageConstant.SHOP_IS_NOT_EXISTS);
        }
        return Result.success(shop);

    }

    //创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 基于逻辑过期的方案解决缓存击穿问题
     * @param id
     * @return
     */
    //因为热点key基本都提前写入redis预热了，所以这里不需要考虑缓存穿透问题
    public Shop findShopByIdWithLogicExpiration(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //根据id到redis里查询店铺缓存数据
        String shopJsonStr = stringRedisTemplate.opsForValue().get(shopKey);
        //1.判断缓存是否命中
        if (StrUtil.isBlank(shopJsonStr)) {
            //1.1缓存未命中，直接返回失败信息
            return null;
        }
        //1.2命中,将shopJsonStr反序列化为Shop对象
        RedisData redisData = JSONUtil.toBean(shopJsonStr, RedisData.class);
        //这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data , Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        //判断缓存是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            //1.1.1未过期，直接返回商铺的过期信息
            return shop;
        }
        //1.1.2已过期，获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLocked = tryToGetMutexLock(lockKey);
        //1.1.2.1获取锁失败，直接返回商铺的过期信息
        if(!isLocked){
            return shop;
        }
        //1.1.2.2获取锁成功，开启独立线程，实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(()->{
            try {
                //重建缓存
                saveShopToRedis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                //释放锁
                freeMutexLock(lockKey);
            }
        });
        //返回数据
        return shop;
    }

    /**
     * 基于互斥锁的方案解决缓存击穿问题
     * @param id
     * @return
     */
    public Shop findShopByIdWithMutex(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //根据id到redis里查询店铺缓存数据
        Result result = judgeShopCache(shopKey);
        if (result != null) {
            return (Shop) result.getData();
        }

        String lockKey = null;
        Shop shop;
        try {
            //不存在，根据id查询数据库
            //解决缓存击穿问题
            //1.获取互斥锁
            lockKey = RedisConstants.LOCK_SHOP_KEY + id;
            boolean isGetLock = tryToGetMutexLock(lockKey);
            //2.判断是否获取锁成功
            if(!isGetLock){
                //3.失败，则休眠并重新判断并获取锁
                Thread.sleep(50);
                findShopByIdWithMutex(id);
            }
            //4.成功获取锁，再次判断缓存是否存在(重要)
            Result newResult = judgeShopCache(shopKey);
            if (newResult != null) {
                return (Shop) newResult.getData();
            }

            //5.成功，根据id查询数据库
            shop = getById(id);
            //测试互斥锁应对高并发的情况，如果执行过程中只调用了一条SQL语句，
            // 那么即使在高并发的情况下，也不会出现缓存穿透的问题。
            // 但是，如果执行过程中调用了多条SQL语句，那么在高并发的情况下，可缓解缓存穿透的问题。
            // 因为在高并发的情况下，多个线程可能会同时尝试获取锁，但是只有一个线程能够获取到锁，
            // 其他线程会被阻塞，从而减少了对数据库的访问压力。

            //模拟缓存重建时的延时
            // Thread.sleep(200);
            //数据库里不存在，返回错误
            if (shop == null) {
                //解决缓存穿透问题
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //数据库里存在，写入redis
            String jsonStr = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(shopKey, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //6.释放锁
            freeMutexLock(lockKey);
        }
        //返回数据
        return shop;
    }

    /**
     * 判断缓存里是否有商店信息
     * @param shopKey
     * @return
     */
    public Result judgeShopCache(String shopKey){
        //根据id到redis里查询店铺缓存数据
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        //存在，直接返回
        if (shopJson != null) {
            if (!StringUtils.isEmpty(shopJson)) {
                //将json数据转换为对象
                Shop shop = JSONUtil.toBean(shopJson, Shop.class);
                return Result.success(shop);
            }else {
                //判断非空（等于""空值）
                //解决缓存穿透问题
                //返回错误信息
                return Result.error(MessageConstant.SHOP_IS_NOT_EXISTS);
            }
        }
        //缓存未命中，既不是空串也没有值
        return null;
    }

    /**
     * 获取互斥锁
     */
    private boolean tryToGetMutexLock(String lockKey) {
        //setIfAbsent()方法是一个原子操作，它会先检查指定的键是否存在，
        //如果不存在，则设置键的值为指定的值，并返回true；如果键已经存在，则不进行任何操作，并返回false
        //BooleanUtil.isTrue()方法用于判断一个布尔值是否为true，防止自动拆箱过程出现空指针
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    public void freeMutexLock(String lockKey){
        stringRedisTemplate.delete(lockKey);
    }

    /**
     * 缓存穿透的解决方案（基于设置空值的方法来解决缓存穿透）
     */
    /**public Shop findShopByIdWithPassThrough(Long id) {
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        //根据id到redis里查询店铺缓存数据
        Result result = judgeShopCache(shopKey);
        if (result != null) {
            return (Shop) result.getData();
        }

        //不存在，根据id查询数据库
        Shop shop = getById(id);
        //数据库里不存在，返回错误
        if (shop == null) {
            //解决缓存穿透问题
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //数据库里存在，写入redis
        String jsonStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(shopKey, jsonStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //返回数据
        return shop;
    }*/

    /**
     * 保存店铺信息到redis中(基于逻辑过期的方法解决缓存击穿问题)
     * @param id
     * @param expireTime
     * @return
     */
    public void saveShopToRedis(Long id, Long expireTime) throws InterruptedException {
        //获取店铺信息
        Shop shop = getById(id);
        //封装RedisData对象
        //模拟缓存重建时的延时，用于测试
        Thread.sleep(200);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
        //返回
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }


    /**
     * 更新商铺信息
     * @param shop
     * @return
     */
    @Transactional
    @Override
    public Result updateByShopId(Shop shop) {
        //判断店铺是否存在
        Long shopId = shop.getId();
        if (shopId == null) {
            return Result.error(MessageConstant.SHOP_IS_NOT_EXISTS);
        }
        //更新数据库
        updateById(shop);
        //删除redis缓存
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shopId);
        //返回
        return Result.success();
    }

    /**
     * 根据商铺类型分页查询商铺信息 - 根据redis距离判断分页查询
     * @param typeId 商铺类型
     * @param current 页码
     * @param x 经度
     * @param y 纬度
     * @return 商铺列表
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 1.判断是否要根据距离查询
        if(x == null || y == null){
            // 1.1不用的话直接根据数据库查询并返回
            Page<Shop> shopPage = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.success(shopPage.getRecords());
        }

        // 2.需要距离查询，先获取分页参数
        // 2.1获取分页起始位置
        Integer from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        // 2.2获取分页结束位置
        Integer end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 3.从redis中查询店铺id和距离 按照距离排序、分页。结果：shopId、distance
        // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
        String key = RedisConstants.SHOP_GEO_KEY +typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo().search(
                key, // shop：geo键
                GeoReference.fromCoordinate(new Point(x, y)), // 中心点坐标
                new Distance(SystemConstants.DEFAULT_DISTANCE_LENGTH), // 查询范围（半径）
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance()// 带上距离返回
                        .limit(end) // 查到最后结尾
        );
        // 3.1进行非空判断
        if(geoResults == null){
            return Result.success(Collections.emptyList());
        }
        // 3.2解析处店铺的id
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> shopIdsContent = geoResults.getContent();
        // 如果没有下一页了，直接返回
        if(shopIdsContent.size() <= from){
            return Result.success(Collections.emptyList());
        }
        // 3.3自定义按照分页范围截取
        List<Long> shopIds = new ArrayList<>(shopIdsContent.size());
        Map<String, Distance> distanceMap = new HashMap<>(shopIdsContent.size());
        shopIdsContent.stream().skip(from).forEach(result -> {
            // 获取店铺的id
            String shopIdStr = result.getContent().getName();
            shopIds.add(Long.valueOf(shopIdStr));
            // 获取距离和对应的店铺id
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });

        // 4.通过店铺id查询店铺集合
        String idsStr = StrUtil.join(",", shopIds);
        List<Shop> shopList = query()
                .in("id", shopIds)
                .last("ORDER BY FIELD (id, " + idsStr + ")")
                .list();
        // 4.1为shop赋值 - 距离
        for (Shop shop : shopList) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 5.返回店铺集合
        return Result.success(shopList);
    }
}
